package cn.demoncat.util.mongo.util;

import cn.demoncat.util.lang.StringUtil;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.mongodb.core.aggregation.*;
import org.springframework.data.mongodb.core.aggregation.BucketAutoOperation.BucketAutoOperationOutputBuilder;
import org.springframework.data.mongodb.core.aggregation.BucketOperation.BucketOperationOutputBuilder;
import org.springframework.data.mongodb.core.aggregation.ConditionalOperators.Cond;
import org.springframework.data.mongodb.core.aggregation.ConditionalOperators.IfNull;
import org.springframework.data.mongodb.core.aggregation.ProjectionOperation.ProjectionOperationBuilder;
import org.springframework.data.mongodb.core.query.Criteria;

import java.util.Map;
import java.util.Map.Entry;
import java.util.function.Function;

/**
 * MongoDB聚合工具
 * 
 * <pre>
 * 通过聚合/管道，执行复杂的操作，效率高于Map-Reduce
 * 
 * 聚合：处理数据并返回计算结果，如平均值、求和等。
 * 管道：将当前聚合的输出结果，作为另一个聚合的输入参数。
 * 
 * 功能说明：https://docs.mongodb.com/manual/aggregation/
 * 集成说明：https://docs.spring.io/spring-data/mongodb/docs/current/reference/html/#mongo.aggregation
 *
 * </pre>
 * 
 * @author 延晓磊
 *
 * @since 2020年8月28日
 */
public class AggregationUtil {
	
	/**
	 * 聚合
	 * 
	 * @param operation	聚合操作
	 * @return
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020年8月29日
	 */
	public static Aggregation build(AggregationOperation... operation) {
		return Aggregation.newAggregation(operation);
	}

	/**
	 * 包含字段
	 * 
	 * @param fields	包含的字段（默认包含_id）
	 * 
	 * @return 投影操作（控制文档结构）
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020年8月29日
	 */
	public static ProjectionOperation include(String... fields) {
		// {$project: {name: 1, title: 1}} >> 1表示展示
		return Aggregation.project(fields);
	}
	
	/**
	 * 排除字段
	 * 
	 * @param fields	排除的字段
	 * 
	 * @return 投影操作（控制文档结构）
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020年8月29日
	 */
	public static ProjectionOperation exclude(String... fields) {
		// {$project: {name: 0, title: 0}} >> 0表示展示
		return new ProjectionOperation().andExclude(fields);
	}
	
	/**
	 * 重命名字段
	 * 
	 * @param operation	投影操作
	 * @param field		字段
	 * @param alias		别名（包含在文档投影）
	 * 
	 * @return 投影操作（控制文档结构）
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020年8月29日
	 */
	public static ProjectionOperation rename(ProjectionOperation operation, String field, String alias) {
		if(operation == null) operation = new ProjectionOperation();
		// $project: {a: 1, b: 1, alias: $field} >> alias表示新增字段，field表示引用原字段
		return operation.and(field).as(alias);
	}
	
	/**
	 * 默认赋值字段
	 * 
	 * @param operation	投影操作
	 * @param field		计算字段
	 * @param value		默认值：字段field为null时，赋默认值value
	 * 
	 * @return 投影操作（控制文档结构）
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020年8月29日
	 */
	public static ProjectionOperation ifNull(ProjectionOperation operation, String field, Object value) {
		return condition(operation, field, IfNull.ifNull(field).then(value));
	}
	
	/**
	 * 条件赋值字段 - 空判断
	 * 
	 * @param operation	投影操作
	 * @param field		计算字段
	 * @param condition		条件控制：如果指定字段为null，赋值field为?
	 * 
	 * @return 投影操作（控制文档结构）
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020年8月29日
	 */
	public static ProjectionOperation condition(ProjectionOperation operation, String field, IfNull condition) {
		if(operation == null) operation = new ProjectionOperation();
		// $project: {field: 计算表达式} >> 将field的值投影为计算表达式的结果（动态取值）
		return operation.and(field).applyCondition(condition);
	}
	
	/**
	 * 条件赋值字段 - 分支判断
	 * 
	 * @param operation	投影操作
	 * @param field		计算字段
	 * @param condition		条件控制：如果指定字段为null，赋值field为?
	 * 
	 * @return 投影操作（控制文档结构）
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020年8月29日
	 */
	public static ProjectionOperation condition(ProjectionOperation operation, String field, Cond condition) {
		if(operation == null) operation = new ProjectionOperation();
		// $project: {field: 计算表达式} >> 将field的值投影为计算表达式的结果（动态取值）
		return operation.and(field).applyCondition(condition);
	}
	
	/**
	 * 计算赋值字段
	 * 
	 * @param operation	投影操作
	 * @param field		计算字段
	 * @param alias		结果字段别名，默认field
	 * @param fn		字段计算：floor, plus 等
	 * 
	 * @return 投影操作（控制文档结构）
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020年8月29日
	 */
	public static ProjectionOperation compute(ProjectionOperation operation, String field, String alias, Function<ProjectionOperationBuilder, ProjectionOperationBuilder> fn) {
		if(operation == null) operation = new ProjectionOperation();
		// $project: {field: 计算表达式} >> 将field的值投影为计算表达式的结果（动态取值）
		return fn.apply(operation.and(field)).as(StringUtil.toDefault(alias, field));
	}
	
	/**
	 * 合并字段到数组
	 * 
	 * @param operation	投影操作
	 * @param alias		别名（数组字段的名称）
	 * @param field		字段（合并到数组的字段）
	 * 
	 * @return 投影操作（控制文档结构）
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020年8月29日
	 */
	public static ProjectionOperation array(ProjectionOperation operation, String alias, String... field) {
		if(operation == null) operation = new ProjectionOperation();
		// { $project: { alias: [ "$x", "$y" ] } }
		return operation.andArrayOf(Fields.fields(field).asList().toArray()).as(alias);
	}
	
	/**
	 * 排序字段
	 * 
	 * @param sort	排序参数 {@link QueryUtil}
	 * @return
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020年8月29日
	 */
	public static SortOperation sort(Sort sort) {
		// {$sort: {id: 1, name: 0}
		return Aggregation.sort(sort);
	}
	
	/**
	 * 排序字段
	 * 
	 * @param field	排序的字段
	 * @return
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020年8月29日
	 */
	public static SortOperation asc(String... field) {
		// {$sort: {name: 1}
		return Aggregation.sort(Direction.ASC, field);
	}
	
	/**
	 * 逆序字段
	 * 
	 * @param field	排序的字段
	 * @return
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020年8月29日
	 */
	public static SortOperation desc(String... field) {
		// {$sort: {name: 0}
		return Aggregation.sort(Direction.DESC, field);
	}
	
	/**
	 * 划分桶（按照数值的片段和范围，将文档划分为多个桶，以便对桶内文档进行独立的聚合操作）
	 * 
	 * @param field			分组字段（数值类型）
	 * @param boundaries	边界（分组字段的分组阀值，必须是升序的：[0,100,400] = [0~100,100~400]）
	 * 
	 * @return 桶操作
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020年8月29日
	 * 
	 * @see <a href="https://docs.mongodb.com/manual/reference/operator/aggregation/bucket/">功能说明</a>
	 */
	public static BucketOperation bucket(String field, Object... boundaries) {
		// {$bucket: {groupBy: $price, boundaries: [0, 100, 400]}}  >> 根据price字段分成2个桶，0~100和100~400
		return Aggregation.bucket(field).withBoundaries(boundaries);
	}
	
	/**
	 * 划分桶（按照数值的片段和范围，将文档划分为多个桶，以便对桶内文档进行独立的聚合操作）
	 * 
	 * @param field			分组字段（数值类型）
	 * @param buckets		分组数（自动确定分组阀值，以保证均匀分布）
	 * 
	 * @return 桶操作
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020年8月29日
	 * 
	 * @see <a href="https://docs.mongodb.com/manual/reference/operator/aggregation/bucketAuto/">功能说明</a>
	 */
	public static BucketAutoOperation autoBucket(String field, int buckets) {
		// {$bucketAuto: {groupBy: $price, buckets: 5}}  >> 根据price字段分成5个桶
		return Aggregation.bucketAuto(field, buckets);
	}
	
	/**
	 * 操作桶 - 统计文档数
	 * 
	 * @param operation	桶操作
	 * @param alias		结果字段别名，默认"count"
	 * @return
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020年8月29日
	 */
	public static BucketOperation bucketCount(BucketOperation operation, String alias) {
		// {$bucket: {groupBy: $price, boundaries: [0, 100], output: { count: { $sum: 1}}}} >> 统计每个桶的文档数，返回到count字段
		return operation.andOutputCount().as(StringUtil.toDefault(alias, "count"));
	}
	
	/**
	 * 操作桶 - 统计文档数
	 * 
	 * @param operation	桶操作
	 * @param alias		结果字段别名，默认"count"
	 * @return
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020年8月29日
	 */
	public static BucketAutoOperation bucketCount(BucketAutoOperation operation, String alias) {
		// {$bucketAuto: {groupBy: $price, boundaries: [0, 100], output: { count: { $sum: 1}}}} >> 统计每个桶的文档数，返回到count字段
		return operation.andOutputCount().as(StringUtil.toDefault(alias, "count"));
	}
	
	/**
	 * 操作桶
	 * 
	 * @param operation	桶操作
	 * @param field		操作字段
	 * @param alias		结果字段别名，默认field
	 * @param fn		字段操作：sum, avg 等
	 * @return
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020年8月29日
	 */
	public static BucketOperation bucketOperation(BucketOperation operation, String field, String alias, Function<BucketOperationOutputBuilder, BucketOperationOutputBuilder> fn) {
		// {$bucket: {groupBy: $price, boundaries: [0, 100], 5, output: { titles: { $push: "$title"}}} >> 处理每个桶的field字段，返回到alias字段
		return fn.apply(operation.andOutput(field)).as(StringUtil.toDefault(alias, field));
	}
	
	/**
	 * 操作桶
	 * 
	 * @param operation	桶操作
	 * @param field		操作字段
	 * @param alias		结果字段别名，默认field
	 * @param fn		字段操作：sum, avg 等
	 * @return
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020年8月29日
	 */
	public static BucketAutoOperation bucketOperation(BucketAutoOperation operation, String field, String alias, Function<BucketAutoOperationOutputBuilder, BucketAutoOperationOutputBuilder> fn) {
		// {$bucketAuto: {groupBy: $price, boundaries: [0, 100], 5, output: { titles: { $push: "$title"}}} >> 处理每个桶的field字段，返回到alias字段
		return fn.apply(operation.andOutput(field)).as(StringUtil.toDefault(alias, field));
	}
	
	/**
	 * 多面聚合
	 * 
	 * 在单个聚合阶段（同一批输入文档），进行多个维度的（子）聚合管道操作，输出多个结果文档（对应子聚合管道的别名）
	 * 
	 * @param operation	多面聚合操作（null表示首个子管道，非null表示增加子管道）
	 * @param alias		子聚合别名
	 * @param agg		子聚合管道
	 * 
	 * @return 
	 * @author 延晓磊
	 *
	 * @since 2020年8月29日
	 * 
	 * @see <a href="https://docs.mongodb.com/manual/reference/operator/aggregation/facet/">功能说明</a>
	 */
	public static FacetOperation facet(FacetOperation operation, String alias, AggregationOperation... agg) {
		// { $facet: { alias: [ agg ] } >> 聚合结果为 { alias: 子管道结果 }
		if(operation == null) operation = Aggregation.facet();
		// alias: [ agg ]
		return Aggregation.facet(agg).as(alias);
	}
	
	/**
	 * 多面聚合
	 * 
	 * 在单个聚合阶段（同一批输入文档），进行多个维度的（子）聚合管道操作，输出多个结果文档（对应子聚合管道的别名）
	 * 
	 * @param aggs		子聚合别名:子聚合管道
	 * 
	 * @return 
	 * @author 延晓磊
	 *
	 * @since 2020年8月29日
	 * 
	 * @see <a href="https://docs.mongodb.com/manual/reference/operator/aggregation/facet/">功能说明</a>
	 */
	public static FacetOperation facet(Map<String, AggregationOperation[]> aggs) {
		// { $facet: { alias: [ agg ] } >> 聚合结果为 { alias: 子管道结果 }
		FacetOperation facet = Aggregation.facet();
		for (Entry<String, AggregationOperation[]> agg : aggs.entrySet()) {
			facet.and(agg.getValue()).as(agg.getKey());
		}
		return facet;
	}
	
	/**
	 * 分组并排序
	 * 
	 * 根据field分组 > 统计每个分组的文档数 > 逆序返回结果 { "_id" : "分组值", "count" : 分组文档数 } == $group > $sort 
	 * 
	 * @return
	 * @param field	分组字段
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020年8月29日
	 */
	public static SortByCountOperation sortByGroup(String field) {
		// { $sortByCount: "$tags" } == { $group: { _id: tags, count: { $sum: 1 } } }, 	{ $sort: { count: -1 } }
		return Aggregation.sortByCount(field);
	}
	
	/**
	 * 统计文档数
	 * 
	 * @param alias
	 * 
	 * @return 
	 * @author 延晓磊
	 *
	 * @since 2020年8月29日
	 */
	public static CountOperation count(String alias) {
		// { $count: "count" } >> 统计文档数，返回在count字段 >> {count : 11}
		return Aggregation.count().as(StringUtil.toDefault(alias, "count"));
	}
	
	/**
	 * 统计文档数
	 * 
	 * @return
	 * @author 延晓磊
	 *
	 * @since 2020年8月29日
	 */
	public static CountOperation count() {
		return count(null);
	}
	
	/**
	 * 分组
	 * 
	 * 根据field的值对文档进行分组 > [对组内数据进行count,sum等运算，指定运算结果的别名] > 返回分组结果 {field1: field1Value, 运算别名: 运算值}
	 * 
	 * @param field	分组字段
	 * 
	 * @return 分组操作
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020年8月29日
	 * 
	 * @see <a href="https://docs.mongodb.com/manual/reference/operator/aggregation/group/#pipe._S_group">Group说明</a>
	 */
	public static GroupOperation group(String... field) {
		//  $group : {_id: field, count: { $sum: 1 } }
		return Aggregation.group(field);
	}
	
	/**
	 * 分组统计 - 统计组内文档数，返回到count字段
	 * 
	 * @param field	分组字段
	 * 
	 * @return
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020年8月29日
	 */
	public static GroupOperation groupCount(String... field) {
		return group(field).count().as("count");
	}
	
	/**
	 * 限制返回的结果数
	 * 
	 * @param limit	最多返回的文档数
	 * 
	 * @return 
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020年8月29日
	 */
	public static LimitOperation limit(long limit) {
		return Aggregation.limit(limit);
	}
	
	/**
	 * 跳过指定个结果
	 * 
	 * @param skip	跳过的文档数
	 * 
	 * @return 
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020年8月29日
	 */
	public static SkipOperation skip(long skip) {
		return Aggregation.skip(skip);
	}
	
	/**
	 * 条件筛选
	 * 
	 * @param cri	筛选条件
	 * 
	 * @return 
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020年8月29日
	 */
	public static MatchOperation match(Criteria cri) {
		return Aggregation.match(cri);
	}
	
	/**
	 * 将聚合结果输出到目标集合（聚合查询并插入，必须是最后的聚合管道）
	 * 
	 * @param collection	目标集合
	 * 
	 * @return 
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020年8月29日
	 */
	public static MergeOperation merge(String collection) {
		return Aggregation.merge().intoCollection(collection).build();
	}
	
	/**
	 * 展开数组（拆分文档，数组属性的值替换为元素的值）
	 * 
	 * @param field		数组字段：如果为空，将不输出文档
	 * 
	 * @return 
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020年8月29日
	 */
	public static UnwindOperation unwind(String field) {
		return Aggregation.unwind(field);
	}
	
}
